#!/usr/bin/perl

##hogger ver 0.2.0 Dev
##parkercrook@gmail.com

# Copyright (C) 2010 Parker Crook

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.


use strict;
use Getopt::Long qw(:config no_ignore_case bundling);
use XML::Writer;
use IO::File;
use Switch;
use Socket;

my ($csv, $nmap, $nmap_dir, $xml, $config_file, $file);
my $readonly = '';
my @files=();
my %oldcsvhash=();
my %csvhash=();
my %nmaphash=();
my %config_info=();

# Create individual xml elements in form <$element>$value</element>
sub xml_element {
    my ($element,$value,$xmlobject)=@_;
    $xmlobject->startTag($element);
    $xmlobject->characters($value);
    $xmlobject->endTag();
}

sub write_xml {
        my ($xml,$hashref)=@_;
        my ($output, $writer);
        $output = new IO::File(">$xml") or die "Cannot open $xml: $!\n";
        $writer = new XML::Writer(OUTPUT => $output, NEWLINES => 0, DATA_MODE => 1, DATA_INDENT => 1, UNSAFE => 1);
        $writer->startTag('SNORT_ATTRIBUTES');
        $writer->startTag('ATTRIBUTE_TABLE');
        foreach (sort keys %$hashref) {
                my $IP = inet_ntoa(pack('N', $_));
                if ($$hashref{$_}{'OS'} =~ /^___/ ) {
                        print ("No OS set, host neglected from xml, please check csv: $IP\n");
                } else {
                        $writer->startTag('HOST');
                        xml_element('IP',$IP,$writer);
                        $writer->startTag('OPERATING_SYSTEM');
                        $writer->startTag('NAME');
                        xml_element('ATTRIBUTE_VALUE',$$hashref{$_}{'OS'},$writer);
                        $writer->endTag('NAME');
                        xml_element('FRAG_POLICY',$$hashref{$_}{'frag3'},$writer);
                        xml_element('STREAM_POLICY',$$hashref{$_}{'stream5'},$writer);
                        $writer->endTag('OPERATING_SYSTEM');
                        $writer->startTag('SERVICES');
                        for my $k (sort keys %{$hashref->{$_}{'services'}}) {
                                $writer->startTag('SERVICE');
                                $writer->startTag('PORT');
                                xml_element('ATTRIBUTE_VALUE',$k,$writer);
                                $writer->endTag();
                                foreach my $k2 (keys %{$$hashref{$_}{'services'}{$k}}) {
                                        $writer->startTag('IPPROTO');
                                        xml_element('ATTRIBUTE_VALUE',$k2,$writer);
                                        $writer->endTag();
                                        $writer->startTag('PROTOCOL');
                                        xml_element('ATTRIBUTE_VALUE',$$hashref{$_}{'services'}{$k}{$k2},$writer);
                                        $writer->endTag();
                                        $writer->endTag();
                                }
                        }
                        $writer->endTag('SERVICES');
                        $writer->endTag('HOST');
                }
        }
        $writer->endTag('ATTRIBUTE_TABLE');
        $writer->endTag('SNORT_ATTRIBUTES');
        $writer->end();
        $output->close();
}

sub read_csv {
        my ($csv,$hashref)=@_;
        open (READ,"<$csv") || die "Unable to read $csv - $!\n";
        while (<READ>) {
                chomp($_);
                if ($_=~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}),,/) {
                        print ("OS field null, host neglected from csv, please check nmap: $1\n");
                }
                        #look at the beginning of the line for an IP, followed by a non-null field
                        #If it's not there it's invalid!
                if ($_=~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3},[\w\s]+,/) {
                        my @data=split(/,/,$_);
                        my $IP=trim(shift(@data));
                        $IP=ip_todec($IP);
                        my $OS=trim(shift(@data));
                        while (@data) {
                                my ($port,$ipproto,$proto) = split(/\|/,shift(@data));
                                $$hashref{$IP}{'services'}{trim($port)}{trim($ipproto)}=trim($proto);
                        } undef @data;
                        switch ($OS){ #assign frag3 & stream5 policy based on OS
                                case /^[Cisco|IOS]/ {
                                        $$hashref{$IP}{'frag3'} = "Last"; 
                                        $$hashref{$IP}{'stream5'} = "last";
                                        $$hashref{$IP}{'OS'} = "IOS";}
                                case /^HP\sJetDirect/ {
                                        $$hashref{$IP}{'frag3'} = "BSD-right"; $$hashref{$IP}{'stream5'} = "bsd"; }
                                case /^HP-UX 10/ {
                                        $$hashref{$IP}{'frag3'} = "BSD"; $$hashref{$IP}{'stream5'} = "hpux10"; }
                                case /^HP-UX 11/ {
                                        $$hashref{$IP}{'frag3'} = "First"; $$hashref{$IP}{'stream5'} = "hpux"; }
                                case /^IRIX/ {
                                        $$hashref{$IP}{'frag3'} = "BSD"; $$hashref{$IP}{'stream5'} = "irix"; }
                                case /^Linux/ {
                                        $$hashref{$IP}{'frag3'} = "Linux"; $$hashref{$IP}{'stream5'} = "linux"; }
                                case /^Mac/ {
                                        $$hashref{$IP}{'frag3'} = "First"; $$hashref{$IP}{'stream5'} = "macos"; }
                                case /^SunOS\s5/ {
                                        $$hashref{$IP}{'frag3'} = "First"; $$hashref{$IP}{'stream5'} = "first"; }
                                case /^Solaris/ {
                                        $$hashref{$IP}{'frag3'} = "Solaris"; $$hashref{$IP}{'stream5'} = "solaris"; }
                                case /^Microsoft Windows Server 200(3|8)/ {
                                        $$hashref{$IP}{'frag3'} = "Windows"; $$hashref{$IP}{'stream5'} = "win2003"; }
                                case /^Microsoft Windows 200(3|8)/ {
                                        $$hashref{$IP}{'frag3'} = "Windows"; $$hashref{$IP}{'stream5'} = "win2003"; }
                                case /^Windows Server 200[3,8]/ {
                                        $$hashref{$IP}{'frag3'} = "Windows"; $$hashref{$IP}{'stream5'} = "win2003"; }
                                case /^Microsoft Windows/ {
                                        $$hashref{$IP}{'frag3'} = "Windows"; $$hashref{$IP}{'stream5'} = "windows"; }
                                case /^Windows/ {
                                        $$hashref{$IP}{'frag3'} = "Windows"; $$hashref{$IP}{'stream5'} = "windows"; }
                                else {
                                        $$hashref{$IP}{'frag3'} = "BSD"; $$hashref{$IP}{'stream5'} = "bsd"; }
                        }
                        $$hashref{$IP}{'OS'} = $OS unless exists $$hashref{$IP}{'OS'};
                }
        } close(READ);
}

sub read_nmap {
        my ($nmap,$hashref)=@_;
        my ($IP);
        open (READ,"<$nmap") || die "Unable to open $nmap - $!";
        while (<READ>) {
                my $nmap_line=$_;
                chomp($nmap_line);          # Get rid of the trailling \n
                if ($nmap_line =~ /((Interesting\sports\son)|(Nmap\sscan\sreport\sfor))\s(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\:?/) {
                        $IP=ip_todec($4);
                } elsif ($nmap_line =~ /^(\d+)\/(\w{2,4})\s*open\s*(\S*)/) {
                        $$hashref{$IP}{'services'}{$1}{$2}=$3;
                } elsif ($nmap_line =~ /^((Service\sInfo:(\sHost:\s)?[-\.\s\d\w]*;?\sOSs?:)|(Running(\s\(JUST\sGUESSING\)\s)?:))\s{1}([\w\s\.\d]*)/ ) {
                        $$hashref{$IP}{'OS'}=$6;
                } elsif ($nmap_line =~ /^No\sOS\smatches\sfor\shost/ ) {
                        $$hashref{$IP}{'OS'}='___';
                }
        }
}

sub write_csv {
        my ($csv,$hashref,$oldhashref)=@_;
        open (WRITE,">$csv") || die "Unable to open $csv - $!";
        foreach (sort keys %$oldhashref) {
                my $temp_IP = ip_todec($_);
                my $key_exists = exists $$hashref{$_};
                my $value_exists = exists $$oldhashref{$_}{'OS'};
                if (($key_exists == 0) and ($value_exists)) { #only add hosts in csv if they do not exist in nmap files.
                        print WRITE inet_ntoa(pack('N', $_)).",".$$oldhashref{$_}{'OS'};
                        foreach my $k (sort keys %{$$oldhashref{$_}{'services'}}){
                                foreach my $k2 (keys %{$$oldhashref{$_}{'services'}{$k}}) {
                                        print WRITE ",".$k."|".$k2."|".svc_cleanup($$oldhashref{$_}{'services'}{$k}{$k2});
                                }
                        }
                        print WRITE "\n";
                }
        }
        foreach (sort keys %$hashref) {
                print WRITE inet_ntoa(pack('N', $_)).",".$$hashref{$_}{'OS'};
                foreach my $k (sort keys %{$$hashref{$_}{'services'}}){
                        foreach my $k2 (keys %{$$hashref{$_}{'services'}{$k}}) {
                                print WRITE ",".$k."|".$k2."|".svc_cleanup($$hashref{$_}{'services'}{$k}{$k2});
                        }
                }
                print WRITE "\n";
        }
        close (WRITE) || die "Unable to write $csv - $!";
}

##Parse config file for name, value pairs
sub config_file_read {
        my ($conf_file, $conf_val) = @_;
        my ($line, $name, $value);
       
        open (CONFIG, "$conf_file") || die "Unable to open $conf_file!";
        while (<CONFIG>) {
                $line=$_;
                chomp($line);
                $line=trim($line);
                if (($line !~ /^#/) && ($line ne "")) {
                        ($name, $value) = split(/=/, $line);
                $$conf_val{$name} = $value;
                }
        } close(CONFIG);
}

sub help {
print<<__EOT;
  Usage: $0 [--readonly -help] -C <conf filename> -n <nmap filename>
     -D <nmap files directory> -c <csv filename> -x <host_attribute filename>

  Options:
  -c Where the human-readable/modifiable csv file containing host information lives.
  -n Where the nmap file containing host information lives.
  -x Where you want to create the host_attribute table.xml (Overwrites existing files)
  -C Where the hogger config file lives.
  -D Direcotry containing multiple nmap files
  --readonly Process the csv file and output to xml for snort, but do not read an nmap file.
  -help/? Print this information

__EOT
exit(0);
}

sub hogger {

print<<__EOT;
*****************************************************
*       http://code.google.com/p/hogger
*       Making Snorty & your network friends...
*               Copyright (C) 2010 Parker Crook
*               parkercrook\@gmail.com
*****************************************************

__EOT
}


sub trim
{
    my ($feed) = @_;
    if ($feed){
        $feed=~s/^\s*//;
        $feed=~s/\s*$//;
        return $feed;
    }
}

#Remove ? from protocols
sub svc_cleanup {
        my ($feed) = @_;
        if ($feed) {
                $feed=~s/\?$//;
                return $feed;
        }
}

# Convert quad IP to decimal IP
sub ip_todec {
        my $ip_address = shift;
        my @octets = split(/\./, $ip_address);
        my $DEC = ($octets[0]*1<<24)+($octets[1]*1<<16)+($octets[2]*1<<8)+($octets[3]);
        return $DEC;
}

hogger();
if($#ARGV==-1){help();}

#read options from command line - takes precedence over config values
GetOptions ("n=s" => \$nmap,
            "c=s" => \$csv,
            "x=s" => \$xml,
            "readonly"   => \$readonly,
            "help|?" => sub { help() },
                        "C=s" => \$config_file,
            "D=s" => \$nmap_dir);

if ($config_file && -f $config_file){
        &config_file_read($config_file, \%config_info);
}
#else { #Removing else here, as this breaks hogger when calling with cli options only.
#       print "You need to specify a valid config file!\n\n";
#       help();
#}

if (!$nmap_dir) {
        $nmap_dir = ($config_info{'nmap_dir'});
        if ((!$nmap) && (!$nmap_dir) && (!$readonly)) {
                print("You need to specify an nmap file, directory, or use the readonly switch!\n\n");
                help();
        }
}

if (!$csv) {
        $csv = ($config_info{'csv'});
        if (!$csv) {
                print("You need to specify a csv file!\n\n");
                help();
        }
}

if (!$xml) {
        $xml = ($config_info{'xml'});
        if (!$xml) {
                print("You need to specify a xml file!\n\n");
                help();
        }
}

if($readonly && -f $csv){
        read_csv($csv, \%csvhash);
} else {
           read_csv($csv, \%oldcsvhash) if -f $csv;
        if (-d $nmap_dir) {
                @files = <$nmap_dir/*>;
                foreach $file (@files) {
                        read_nmap($file, \%nmaphash);
                }
        } if ($nmap && -f $nmap) {
                read_nmap($nmap, \%nmaphash);
        }
        write_csv($csv, \%nmaphash, \%oldcsvhash);
        read_csv($csv, \%csvhash);
}
write_xml($xml, \%csvhash);

print ("\nGo Snorty, Go!\n");

__END__
